/*
Copyright 2019 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package inventory

import (
	"encoding/json"

	"sigs.k8s.io/kustomize/pkg/resid"
)

//Refs is a reference map.  Each key is the id
//of a k8s resource, and each value is a list of
//object ids that refer back to the object in the
//key.

//For example, the key could correspond to a
//ConfigMap, and the list of values might include
//several different Deployments that get data from
//that ConfigMap (and thus refer to it).

//References are important in inventory management
//because one may not delete an object before all
//objects referencing it have been removed.
type Refs map[resid.ItemId][]resid.ItemId

func NewRefs() Refs {
	return Refs{}
}

// Merge merges a Refs into an existing Refs
func (rf Refs) Merge(b Refs) Refs {
	for key, value := range b {
		_, ok := rf[key]
		if ok {
			rf[key] = append(rf[key], value...)
		} else {
			rf[key] = value
		}
	}
	return rf
}

// removeIfContains removes the reference relationship
//  a --> b
// from the Refs if it exists
func (rf Refs) RemoveIfContains(a, b resid.ItemId) {
	refs, ok := rf[a]
	if !ok {
		return
	}
	for i, ref := range refs {
		if ref.Equals(b) {
			rf[a] = append(refs[:i], refs[i+1:]...)
			break
		}
	}
}

//Inventory is a an object intended for
//serialization into the annotations of a so-called
//apply-root object (a ConfigMap, an Application,
//etc.) living in the cluster.  This apply-root
//object is written as part of an apply operation as
//a means to record overall cluster state changes.

//At the end of a successful apply, the "current"
//field in Inventory will be a map whose keys all
//correspond to an object in the cluster, and
//"previous" will be the previous such set (an empty
//set on the first apply).

//An Inventory allows the Prune method to work.
type Inventory struct {
	Current  Refs `json:"current,omitempty"`
	Previous Refs `json:"previous,omitempty"`
}

// NewInventory returns an Inventory object
func NewInventory() *Inventory {
	return &Inventory{
		Current:  NewRefs(),
		Previous: NewRefs(),
	}
}

// UpdateCurrent updates the Inventory given a
// new current Refs
// The existing Current refs is merged into
// the Previous refs
func (a *Inventory) UpdateCurrent(curref Refs) *Inventory {
	if len(a.Previous) > 0 {
		a.Previous.Merge(a.Current)
	} else {
		a.Previous = a.Current
	}
	a.Current = curref
	return a
}

func (a *Inventory) removeNewlyOrphanedItemsFromPrevious() []resid.ItemId {
	var results []resid.ItemId
	for item, refs := range a.Previous {
		if _, ok := a.Current[item]; ok {
			delete(a.Previous, item)
			continue
		}

		var newRefs []resid.ItemId
		toDelete := true
		for _, ref := range refs {
			if _, ok := a.Current[ref]; ok {
				toDelete = false
				newRefs = append(newRefs, ref)
			}
		}
		if toDelete {
			results = append(results, item)
			delete(a.Previous, item)
		} else {
			a.Previous[item] = newRefs
		}
	}
	return results
}

func (a *Inventory) removeOrphanedItemsFromPreviousThatAreNotInCurrent() []resid.ItemId {
	var results []resid.ItemId
	for item, refs := range a.Previous {
		if _, ok := a.Current[item]; ok {
			continue
		}
		if len(refs) == 0 {
			results = append(results, item)
			delete(a.Previous, item)
		}
	}
	return results
}

func (a *Inventory) removeOrphanedItemsFromPreviousThatAreInCurrent() {
	//Remove references from Previous that are already in Current refs
	for item, refs := range a.Current {
		for _, ref := range refs {
			a.Previous.RemoveIfContains(item, ref)
		}
	}
	//Remove items from Previous that are already in Current refs
	for item, refs := range a.Previous {
		if len(refs) == 0 {
			if _, ok := a.Current[item]; ok {
				delete(a.Previous, item)
			}
		}
	}
}

// Prune computes the diff of Current refs and Previous refs
// and returns a list of Items that can be pruned.
// An item that can be pruned shows up only in Previous refs.
// Prune also updates the Previous refs with those items removed
func (a *Inventory) Prune() []resid.ItemId {
	a.removeOrphanedItemsFromPreviousThatAreInCurrent()

	// These are candidates for deletion from the cluster.
	removable1 := a.removeOrphanedItemsFromPreviousThatAreNotInCurrent()
	removable2 := a.removeNewlyOrphanedItemsFromPrevious()
	return append(removable1, removable2...)
}

// inventory is the internal type used for serialization
type inventory struct {
	Current  map[string][]resid.ItemId `json:"current,omitempty"`
	Previous map[string][]resid.ItemId `json:"previous,omitempty"`
}

func (a *Inventory) toInternalType() inventory {
	prev := map[string][]resid.ItemId{}
	curr := map[string][]resid.ItemId{}
	for id, refs := range a.Current {
		curr[id.String()] = refs
	}
	for id, refs := range a.Previous {
		prev[id.String()] = refs
	}
	return inventory{
		Current:  curr,
		Previous: prev,
	}
}

func (a *Inventory) fromInternalType(i *inventory) {
	for s, refs := range i.Previous {
		a.Previous[resid.FromString(s)] = refs
	}
	for s, refs := range i.Current {
		a.Current[resid.FromString(s)] = refs
	}
}

func (a *Inventory) marshal() ([]byte, error) {
	return json.Marshal(a.toInternalType())
}

func (a *Inventory) unMarshal(data []byte) error {
	inv := &inventory{
		Current:  map[string][]resid.ItemId{},
		Previous: map[string][]resid.ItemId{},
	}
	err := json.Unmarshal(data, inv)
	if err != nil {
		return err
	}
	a.fromInternalType(inv)
	return nil
}

// UpdateAnnotations update the annotation map
func (a *Inventory) UpdateAnnotations(annot map[string]string) error {
	data, err := a.marshal()
	if err != nil {
		return err
	}
	annot[InventoryAnnotation] = string(data)
	return nil
}

// LoadFromAnnotation loads the Inventory date from the annotation map
func (a *Inventory) LoadFromAnnotation(annot map[string]string) error {
	value, ok := annot[InventoryAnnotation]
	if ok {
		return a.unMarshal([]byte(value))
	}
	return nil
}
